package hydrap2p.library;

import hydrap2p.helper.Utility;
import hydrap2p.library.listening.ListenerManager;
import hydrap2p.logging.Level;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public final class SharedFolders{
	private SafeDatabase safedb;
	private Database db;
	private ListenerManager listeners;
	private List<ItemInfo> scanqueue;
	private List<Boolean> forced;
	
	private SharedFolderCrawler scanningthread;
	
	public SharedFolders()
	{
		try {
			safedb=SafeDatabase.getInstance();
			db=Database.getInstance();
			listeners=ListenerManager.getInstance();
			scanqueue=new LinkedList<ItemInfo>();
			forced=new LinkedList<Boolean>();
		} catch (Exception e) {
			e.printStackTrace();
		}
		scanningthread=new SharedFolderCrawler();
		new Thread(scanningthread).start();
	}

	private static void log(Level level, String source, String reason)
	{
		LibraryLogging.log(level, source, reason);
	}
	
	public void shutdown()
	{
		scanningthread.stopRunning();
		while (scanningthread.isRunning());
	}
	
	public long addSharedFolder(String path) throws FileNotFoundException
	{
		File tempfile=new File(path);
		if (!tempfile.exists())
			throw new FileNotFoundException();
		
		// Check to see if this path is already shared somehow
		List<ItemInfo> found=safedb.searchExactPathFilename(tempfile.getParent(),tempfile.getName());
		if (found.size()>0)
			return found.get(0).uid;
		ItemInfo temp=safedb.create();
		
		temp.pathname=tempfile.getParent();
		temp.name=tempfile.getName();
		temp.isDirectory=tempfile.isDirectory();
		safedb.save(temp);
		rescan(temp,true);
		
		return temp.uid;
	}
	
	public void deleteSharedFolder(long uid)
	{
		new Thread(new SharedFolderDeleter(uid)).start();
	}
	
	public void rescan(boolean force)
	{
		for (ItemInfo item : db.loadSharedFolderList())
			rescan(item, force);
	}
	
	public void rescan(long uid, boolean force)
	{
		rescan(safedb.load(uid, false),force);
	}
	
	public void rescan(ItemInfo node, boolean force)
	{
		if (!node.isDirectory)
			log(Level.INFO,"Library Shared Folders", "Not enqueueing "+node.toPathFile()+" to be scanned, because it is a file");
		log(Level.INFO,"Library Shared Folders","Enqueueing "+node.toPathFile()+" to be scanned");
		synchronized(scanqueue)
		{
			scanqueue.add(node);
			forced.add(force);
			scanqueue.notify();
		}
	}
	
	public boolean isScanning()
	{
		return scanningthread.isScanning();
	}
	
	private class SharedFolderDeleter implements Runnable
	{
		private long uid;
		public SharedFolderDeleter(long uid)
		{
			this.uid=uid;
		}
		@Override
		public void run() {
			delete(uid);
		}
		public void delete(long uid)
		{
			ItemInfo toDelete=safedb.load(uid, true);
			for (long childuid:toDelete.children)
			{
				delete(childuid);
			}
			safedb.delete(toDelete);
		}
	}
	
	private class SharedFolderCrawler implements Runnable
	{
		private MyBoolean running;
		private MyBoolean scanning;
		
		public SharedFolderCrawler()
		{
			running=new MyBoolean(false);
			scanning=new MyBoolean(false);
		}
		@Override
		public void run() {
			ItemInfo temp=null;
			boolean tempforced=false;
			running.setState(true);
			while (isRunning())
			{
				synchronized(scanqueue)
				{
					if (isRunning() && scanqueue.size()==0)
					{
						setScanning(false);
						try {
							scanqueue.wait();
						} catch (InterruptedException e) {}
					}
					if (isRunning() && scanqueue.size()>0)
					{
						setScanning(true);
						temp=scanqueue.remove(0);
						tempforced=forced.remove(0);
					}
					else
						temp=null;
				}
				if (temp!=null)
				{
					scanDir(temp,tempforced);
					temp=null;
				}
			}
		}
		
		public boolean isRunning()
		{
			boolean ret;
			synchronized(running)
			{
				ret=running.getState();
			}
			return ret;
		}
		
		public void stopRunning()
		{
			synchronized(running)
			{
				running.setState(false);
			}
			synchronized(scanqueue)
			{
				scanqueue.notify();
			}
		}
		
		private void setScanning(boolean scanning)
		{
			synchronized(this.scanning)
			{
				this.scanning.setState(scanning);
			}
		}
		public boolean isScanning()
		{
			boolean ret=false;
			synchronized(this.scanning)
			{
				ret=this.scanning.getState();
			}
			return ret;
		}
		
		/**
		 * Given a list of ItemInfo, find the one that matches the given pathname
		 * @param list
		 * @param pathname
		 * @return
		 */
		private ItemInfo ItemInfoListFindPathName(Collection<ItemInfo> list, String pathname)
		{
			for (ItemInfo item:list)
			{
				if (pathname.equals(Utility.appendPath(item.pathname, item.name)))
					return item;
			}
			return null;
		}
		
		private void scanDir(ItemInfo node, boolean force)
		{
			node=safedb.load(node.uid, true);
			log(Level.DEBUG,"Library Shared Folders","Scanning "+node.toPathFile());
			
			// Load known children
			Map<Long, ItemInfo> knownchildren=new TreeMap<Long,ItemInfo>();
			for (long childuid:node.children)
			{
				knownchildren.put(childuid, safedb.load(childuid, false));
			}
			
			// Look for new children
			File file=new File(Utility.appendPath(node.pathname, node.name));
			if (!file.exists())
			{
				log(Level.MISHAP,"Library Shared Folders","Directory "+file.getAbsolutePath()+" does not exist!");
				return;
			}
			if (!file.isDirectory())
			{
				log(Level.MISHAP,"Library Shared Folders","Erroneous attempt to scan "+file.getAbsolutePath()+" for shared files");
				return;
			}
			for (File child : file.listFiles())
			{
				ItemInfo found=ItemInfoListFindPathName(knownchildren.values(),child.getAbsolutePath());
				if (found==null)	// we don't know about this node yet
				{
					log(Level.INFO,"Library Shared Folders","Found new child: "+child.getAbsolutePath());
					
					// make a new child for it
					ItemInfo newchild=safedb.create();
					newchild.pathname=child.getParent();
					newchild.name=child.getName();
					if (child.isDirectory())
					{
						newchild.isDirectory=true;
						safedb.save(newchild);
						rescan(newchild,true);		// new directory, force rescan
					}
					else
					{
						newchild.isDirectory=false;
						safedb.save(newchild);
						scanFile(newchild, true);		// new file, always force rescan everything, though it should anyways because it's blank
					}
					node.children.add(newchild.uid);
					listeners.newSharedItem(newchild);
				}
				if (found!=null)		// modified after lastscan, or forced, or is a directory
				{
					if (child.isDirectory())
					{
						rescan(found, force);
					}
					else
					{
						scanFile(found, force);
					}
				}
			}
			safedb.save(node);
		}
		
		/**
		 * Scans this ItemInfo for new metadata
		 * If force, clear out old metadata and scan against all metadaters, not just new ones
		 * Will lock and release the ItemInfo internally
		 * @param item
		 * @param force
		 */
		private void scanFile(ItemInfo item, boolean force)
		{
			Metadaters metadaters=Metadaters.getInstance();
			Hashers hashers=Hashers.getInstance();
			
			File actualFile=new File(item.toPathFile());
			
			ItemInfo temp=safedb.load(item.uid, true);
			
			boolean filechanged=
				(
					temp.lastscan==null ||
					actualFile.lastModified()>temp.lastscan.getTime()
				);
			boolean forceMetadata=force || filechanged;
			boolean forceHashes=force || filechanged;

			log(Level.DEBUG,"Library Shared Folders","Scanning file "+temp.toPathFile()+" forced:"+force+" changed:"+filechanged);
			boolean changedMetadata=metadaters.scanMetadata(temp, forceMetadata);
			boolean changedHashes=hashers.scanHashes(temp, forceHashes);
			
			temp.lastscan=new Date();	// set the lastscan to now
			safedb.save(temp);
			
			if (changedMetadata)
				listeners.changedMetadata(item);
		}
	}
	
	
}
